/**
 * 引用 modbus-serial 模块
 * @link https://github.com/yaacov/node-modbus-serial
 */
import ModbusSerial from 'modbus-serial';
import { ModbusTCPClientInterface, Register, RegisterAll, Package, PackageItem, ResultInterface } from './interface';
import { EnumRegisterType, EnumDataType, RegisterName } from './enum';
import { ReadCoilResult, ReadRegisterResult } from 'modbus-serial/ModbusRTU';
export default class ModbusTCPClient {
    /** 创建一个新的 client */
    private client: ModbusSerial = new ModbusSerial();
    /** 主机地址 */
    host: string;
    /** 端口号 */
    port: number;
    /** 设备ID */
    deviceID: number;
    /** 连接超时时间，单位：毫秒 */
    timeout: number = 3000;
    /** length of one package */
    packageLength: number = 100;
    /** 最大重连时长，单位：小时，0为无限时长 */
    maxReconnectTime: number = 0;
    /** 尝试重连时间，单位：秒 */
    tryReconnectTime: number = 3;
    /** 所有寄存器 */
    registerMap: Map<string, RegisterAll> = new Map();
    /** 寄存器包 */
    private registerPackage: Package = { Coil: [], DisInput: [], Input: [], Holding: [] };
    /** 轮询间隔时间，单位：毫秒 */
    pollingInterval: number = 1000;
    /** 初次连接失败时间戳 */
    private firstConnFailTime: number = 0;

    constructor(option: ModbusTCPClientInterface) {
        this.host = option.host;
        this.port = option.port;
        this.deviceID = option.deviceID;
        this.timeout = option.timeout === undefined ? this.timeout : option.timeout;
        this.packageLength = option.packageLength === undefined ? this.packageLength : option.packageLength;
        this.maxReconnectTime = option.maxReconnectTime === undefined ? this.maxReconnectTime : option.maxReconnectTime;
        this.tryReconnectTime = option.tryReconnectTime === undefined ? this.tryReconnectTime : option.tryReconnectTime;
    }

    /**
     * 连接设备
     */
    private async connect() {
        return await this.client.connectTCP(this.host, { port: this.port })
            .then(() => {
                this.client.setID(this.deviceID);
                this.client.setTimeout(this.timeout);
                console.log(`connect ${this.host}:${this.port} ${this.deviceID} success ${new Date()}`);
                return this.client;
            })
            .catch(e => {
                return e.message
            });
    }

    /**
     * 断开连接
     */
    disconnect() {
        this.client.close((closeRes: any) => { return closeRes });
    }

    /**
     * 将设置的寄存器数组进行解析整合并排序，然后存入 Map 中
     * @param registerArr 寄存器数组
     */
    private sortRegisters(registerArr: Register[]) {
        let sortedRegisterArr = [];
        for (let i = 0; i < registerArr.length; i++) {
            let tempRegister: RegisterAll = {
                register: registerArr[i].register,
                registerType: EnumRegisterType.Coil,
                startAddr: 0,
                endAddr: 0,
                dataType: EnumDataType.Bit,
                packageIndex: 0,
                bufferAddr: [],
                value: null,
                timestamp: new Date(),
                quality: ''
            };

            // 寄存器类型
            let registerType = registerArr[i].register.substr(0, 1);
            tempRegister.registerType = RegisterName[registerType];

            // 起始地址
            tempRegister.startAddr = +registerArr[i].register.substr(1);

            // 结束地址
            switch (registerArr[i].dataType) {
                case EnumDataType.Bit:
                case EnumDataType.Binary:
                case EnumDataType.Decimal:
                case EnumDataType.Int:
                case EnumDataType.Hex:
                    tempRegister.endAddr = tempRegister.startAddr;
                    break;
                case EnumDataType.Float:
                case EnumDataType.SwappedFloat:
                case EnumDataType.Long:
                case EnumDataType.SwappedLong:
                    tempRegister.endAddr = tempRegister.startAddr + 1;
                    break;
                case EnumDataType.Double:
                case EnumDataType.SwappedDouble:
                    tempRegister.endAddr = tempRegister.startAddr + 3;
                    break;
                default:
                    tempRegister.endAddr = tempRegister.startAddr;
            }

            // 数据类型
            tempRegister.dataType = registerArr[i].dataType;

            sortedRegisterArr.push(tempRegister);
        }

        // 按照寄存器的其实地址从小到大排序
        sortedRegisterArr = sortedRegisterArr.sort(sortRegister);

        for (let i = 0; i < sortedRegisterArr.length; i++) {
            this.registerMap.set(sortedRegisterArr[i].register, sortedRegisterArr[i]);
        }

        function sortRegister(r1: RegisterAll, r2: RegisterAll) {
            return r1.startAddr - r2.startAddr;
        }
    }

    /**
     * 根据排序后的寄存器数组，创建打包信息 this.registerPackage 和一部分寄存器信息，准备读取
     */
    private createPack() {
        for (let register of this.registerMap.values()) {
            let registerType = register.registerType;
            let packageItemArr: PackageItem[];
            let packageLength: number;
            switch (registerType) {
                case EnumRegisterType.Coil:
                    packageItemArr = this.registerPackage.Coil;
                    packageLength = this.packageLength * 16;
                    break;
                case EnumRegisterType.DiscreteInput:
                    packageItemArr = this.registerPackage.DisInput;
                    packageLength = this.packageLength * 16;
                    break;
                case EnumRegisterType.Input:
                    packageItemArr = this.registerPackage.Input;
                    packageLength = this.packageLength;
                    break;
                case EnumRegisterType.Holding:
                    packageItemArr = this.registerPackage.Holding;
                    packageLength = this.packageLength;
                    break;
                default:
                    packageItemArr = this.registerPackage.Coil;
                    packageLength = this.packageLength * 16;
            }

            // 当前寄存器开始地址
            let startAddr = register.startAddr;
            // 当前寄存器结束地址
            let endAddr = register.endAddr;

            if (packageItemArr.length === 0) {
                // 包中还没项 
                let packageItem: PackageItem = {
                    startAddr: startAddr,
                    endAddr: endAddr,
                    length: endAddr - startAddr + 1,
                    datas: null,
                    timestamp: new Date(),
                    quality: ''
                }
                packageItemArr.push(packageItem);
            } else {
                // 已经有项了
                // 获取最后一个包的索引号
                let packageIndex = packageItemArr.length - 1;
                if (endAddr - packageItemArr[packageIndex].startAddr >= packageLength) {
                    // 如果当前寄存器的结束地址 - 包的开始地址 >= 包的长度
                    // 直接新开一个包
                    let packageItem: PackageItem = {
                        startAddr: startAddr,
                        endAddr: endAddr,
                        length: endAddr - startAddr + 1,
                        datas: null,
                        timestamp: new Date(),
                        quality: ''
                    }
                    packageItemArr.push(packageItem);
                } else {
                    // 否则
                    if (endAddr > packageItemArr[packageIndex].endAddr) {
                        // 如果当前寄存器的结束地址 > 当前包的结束地址
                        // 把当前寄存器的结束地址作为当前包的结束地址
                        packageItemArr[packageIndex].endAddr = endAddr;
                        packageItemArr[packageIndex].length = endAddr - packageItemArr[packageIndex].startAddr + 1;
                    }
                }
            }
            // 包索引号生成
            register.packageIndex = packageItemArr.length - 1;

            // 数据长度，单位：字 Word
            let dataLength = 1;
            switch (register.dataType) {
                case EnumDataType.Bit:
                case EnumDataType.Binary:
                case EnumDataType.Decimal:
                case EnumDataType.Hex:
                    dataLength = 0;
                    break;
                case EnumDataType.Int:
                    dataLength = 1;
                    break;
                case EnumDataType.Float:
                case EnumDataType.SwappedFloat:
                case EnumDataType.Long:
                case EnumDataType.SwappedLong:
                    dataLength = 2;
                    break;
                case EnumDataType.Double:
                case EnumDataType.SwappedDouble:
                    dataLength = 4;
                    break;
                default:
                    dataLength = 0;
            }
            // buffer 地址是按字节 Byte 算的
            // buffer 起始地址、结束地址生成
            let bufferStartAddr: number, bufferEndAddr: number;
            if (dataLength !== 0) {
                bufferStartAddr = (startAddr - packageItemArr[register.packageIndex].startAddr) * 2;
                bufferEndAddr = bufferStartAddr + dataLength * 2 - 1;
            } else {
                bufferStartAddr = startAddr - packageItemArr[register.packageIndex].startAddr;
                bufferEndAddr = bufferStartAddr;
            }

            // bufferAddr 生成
            register.bufferAddr.push(bufferStartAddr, bufferEndAddr);
        }
    }

    /**
     * 设置准备要进行数据读取的寄存器
     * @param registerArr 准备读取的寄存器数据
     */
    setRegisters(registerArr: Register[]) {
        this.sortRegisters(registerArr);
        this.createPack();
        return this;
    }

    /**
     * 对数据包进行读取
     */
    private async readData() {
        let promiseArr: Promise<ReadCoilResult | ReadRegisterResult | void>[] = [];
        let coilPakArr = this.registerPackage.Coil;
        let disInputPakArr = this.registerPackage.DisInput;
        let inputPakArr = this.registerPackage.Input;
        let holdingPakArr = this.registerPackage.Holding;

        // 读取线圈包
        for (let i = 0; i < coilPakArr.length; i++) {
            // 数据包起始地址
            let startAddr = coilPakArr[i].startAddr - 1;
            // 数据包长度
            let length = coilPakArr[i].length;

            let readPromise: Promise<ReadCoilResult | void> = this.client.readCoils(startAddr, length)
                .then(datas => {
                    this.registerPackage.Coil[i].datas = datas;
                    coilPakArr[i].timestamp = new Date();
                    coilPakArr[i].quality = 'GOOD';
                    return datas;
                })
                .catch(e => {
                    coilPakArr[i].timestamp = new Date();
                    coilPakArr[i].quality = e.message;
                });

            promiseArr.push(readPromise);
        }
        // 读取离散输入包
        for (let i = 0; i < disInputPakArr.length; i++) {
            // 数据包起始地址
            let startAddr = disInputPakArr[i].startAddr - 1;
            // 数据包长度
            let length = disInputPakArr[i].length;

            let readPromise: Promise<ReadCoilResult | void> = this.client.readDiscreteInputs(startAddr, length)
                .then(datas => {
                    this.registerPackage.DisInput[i].datas = datas;
                    disInputPakArr[i].timestamp = new Date();
                    coilPakArr[i].quality = 'GOOD';
                })
                .catch(e => {
                    disInputPakArr[i].timestamp = new Date();
                    disInputPakArr[i].quality = e.message;
                });

            promiseArr.push(readPromise);
        }
        // 读取输入包
        for (let i = 0; i < inputPakArr.length; i++) {
            // 数据包起始地址
            let startAddr = inputPakArr[i].startAddr - 1;
            // 数据包长度
            let length = inputPakArr[i].length;

            let readPromise: Promise<ReadRegisterResult | void> = this.client.readInputRegisters(startAddr, length)
                .then(datas => {
                    this.registerPackage.Input[i].datas = datas;
                    inputPakArr[i].timestamp = new Date();
                    inputPakArr[i].quality = 'GOOD';
                })
                .catch(e => {
                    inputPakArr[i].timestamp = new Date();
                    inputPakArr[i].quality = e.message;
                });

            promiseArr.push(readPromise);
        }
        // 读取保持包
        for (let i = 0; i < holdingPakArr.length; i++) {
            // 数据包起始地址
            let startAddr = holdingPakArr[i].startAddr - 1;
            // 数据包长度
            let length = holdingPakArr[i].length;

            let readPromise: Promise<ReadRegisterResult | void> = this.client.readHoldingRegisters(startAddr, length)
                .then(datas => {
                    this.registerPackage.Holding[i].datas = datas;
                    holdingPakArr[i].timestamp = new Date();
                    holdingPakArr[i].quality = 'GOOD';
                })
                .catch(e => {
                    holdingPakArr[i].timestamp = new Date();
                    holdingPakArr[i].quality = e.message;
                });

            promiseArr.push(readPromise);
        }

        // 当所有数据读取完毕之后对数据进行派发，派发到结果集中
        await Promise.all(promiseArr).then(() => { this.distribute() });
    }

    /**
     * 分派数据值到 registerMap 的 value、timestamp、quality 中，并返回数据
     */
    private distribute() {
        for (let register of this.registerMap.values()) {
            // 数据类型
            let dataType = register.dataType;
            // buffer 地址数组
            let bufferAddr = register.bufferAddr;
            // 包索引号
            let packageIndex = register.packageIndex;
            // 寄存器类型
            let registerType = '';
            switch (register.registerType) {
                case EnumRegisterType.Coil:
                    registerType = "Coil";
                    break;
                case EnumRegisterType.DiscreteInput:
                    registerType = "DisInput";
                    break;
                case EnumRegisterType.Input:
                    registerType = "Input";
                    break;
                case EnumRegisterType.Holding:
                    registerType = "Holding";
                    break;
                default:
                    registerType = "Coil";
            }
            register.timestamp = this.registerPackage[registerType][packageIndex].timestamp;
            register.quality = this.registerPackage[registerType][packageIndex].quality;
            // 包读取完成之后生成的 buffer 数据
            let bufferDatas = this.registerPackage[registerType][packageIndex].datas;
            // 一些中间变量
            let bufferArr: number[], sortedBufferArr: number[], newBuffer: Buffer;
            // 根据数据类型生成最终要返回的数据
            if (bufferDatas) {
                switch (dataType) {
                    case EnumDataType.Bit:
                        register.value = bufferDatas.data[bufferAddr[0]];
                        break;
                    case EnumDataType.Binary:
                        register.value = bufferDatas.data[bufferAddr[0]].toString(2);
                        break;
                    case EnumDataType.Decimal:
                        register.value = bufferDatas.data[bufferAddr[0]];
                        break;
                    case EnumDataType.Int:
                        register.value = bufferDatas.buffer.readInt16BE(bufferAddr[0]);
                        break;
                    case EnumDataType.Hex:
                        register.value = bufferDatas.data[bufferAddr[0]].toString(16);
                        break;
                    case EnumDataType.Float:
                        bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
                        // 改变顺序 0123 => 2301
                        sortedBufferArr = [bufferArr[2], bufferArr[3], bufferArr[0], bufferArr[1]];
                        newBuffer = Buffer.from(sortedBufferArr);
                        register.value = newBuffer.readFloatBE(0);
                        break;
                    case EnumDataType.SwappedFloat:
                        bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
                        // 顺序 0123 即可
                        newBuffer = Buffer.from(bufferArr);
                        register.value = newBuffer.readFloatBE(0);
                        break;
                    case EnumDataType.Long:
                        bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
                        // 改变顺序 0123 => 2301
                        sortedBufferArr = [bufferArr[2], bufferArr[3], bufferArr[0], bufferArr[1]];
                        newBuffer = Buffer.from(sortedBufferArr);
                        register.value = newBuffer.readInt32BE(0);
                        break;
                    case EnumDataType.SwappedLong:
                        bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
                        // 顺序 0123 即可
                        newBuffer = Buffer.from(bufferArr);
                        register.value = newBuffer.readInt32BE(0);
                        break;
                    case EnumDataType.Double:
                        bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
                        // 改变顺序 0123 4567 => 6745 2301
                        sortedBufferArr = [bufferArr[6], bufferArr[7], bufferArr[4], bufferArr[5], bufferArr[2], bufferArr[3], bufferArr[0], bufferArr[1]];
                        newBuffer = Buffer.from(sortedBufferArr);
                        register.value = newBuffer.readDoubleBE(0);
                        break;
                    case EnumDataType.SwappedDouble:
                        bufferArr = bufferDatas.buffer.slice(bufferAddr[0], bufferAddr[1] + 1);
                        // 顺序 0123 4567 即可
                        newBuffer = Buffer.from(bufferArr);
                        register.value = newBuffer.readDoubleBE(0);
                        break;
                    default:
                        register.value = bufferDatas.data[bufferAddr[0]];
                }
            }
        }
    }

    /**
     * 读取数据一次，然后断开
     */
    async readOnce() {
        let connRes = await this.connect();
        if (this.client.isOpen === true) {
            await this.readData();
            this.disconnect();
            return this.registerMap;
        } else {
            return connRes;
        }
    }

    /**
     * 轮询，外部调用
     * @param {Number} interval 轮询间隔，单位：毫秒
     */
    async polling(interval?: number) {
        let res: ResultInterface = {
            result: false,
            info: null
        }
        if (interval !== undefined) {
            // 如果未传入数字 Interval，那么使用默认值
            this.pollingInterval = interval;
        }
        if (this.client.isOpen === true) {
            await this.readData();
            setTimeout(() => { this.polling() }, this.pollingInterval);
            res.result = true;
            res.info = this.registerMap;
            return res;
        } else {
            let connRes = await this.connect();
            if (typeof connRes !== 'string') {
                await this.readData();
                setTimeout(() => { this.polling() }, this.pollingInterval);
                res.result = true;
                res.info = this.registerMap;
                return res;
            } else {
                await this.reconnect();
                res.info = 'reconnecting';
                return res;
            }
        }
    }

    /**
     * 重连
     */
    private async reconnect() {
        console.log(`try to reconnect ${this.host}:${this.port} ${this.deviceID} ${new Date()}`);
        const connRes = await this.connect();
        if (typeof connRes !== 'string') {
            // 重连成功
            this.firstConnFailTime = 0;
            this.polling();
        } else {
            // 重连失败
            if (this.firstConnFailTime === 0) {
                this.firstConnFailTime = new Date().getTime();
            }

            if (this.maxReconnectTime !== 0) {
                // 最大尝试重连时长不为无限
                if (new Date().getTime() - this.firstConnFailTime <= this.maxReconnectTime * 60 * 60 * 1000) {
                    // 未超过最大尝试重连时间，继续重连
                    setTimeout(async () => {
                        await this.reconnect();
                    }, this.tryReconnectTime * 1000);
                } else {
                    console.log(`${this.host}:${this.port} ${this.deviceID} ${new Date()} max reconnect time has come, connetion closed`);
                }
            } else {
                // 最大尝试重连时长为无限
                setTimeout(async () => {
                    await this.reconnect();
                }, this.tryReconnectTime * 1000);
            }
        }
    }

    /**
     * 写数据
     * @param register 寄存器
     * @param value 要写的值
     */
    async writeData(register: string, value: boolean | number | string | null) {
        let reg = this.registerMap.get(register);
        let result: ResultInterface = {
            result: true,
            info: '',
        };

        if (reg === undefined) {
            // 从设置的寄存器表中也没有找到此寄存器信息
            result.result = false;
            result.info = 'register is not exist';
        } else {
            // 找到了寄存器
            let regAddr = reg.startAddr - 1;
            let regType = reg.registerType;
            let val: any;
            if (regType === EnumRegisterType.Coil) {
                if (typeof value !== 'boolean') {
                    result.result = false;
                    result.info = 'wrong value type';
                } else {
                    val = value;
                }
            } else if (reg.registerType === EnumRegisterType.Holding) {
                switch (reg.dataType) {
                    case EnumDataType.Decimal:
                    case EnumDataType.Int:
                        if (typeof value !== 'number' || value % 1 !== 0) {
                            // 不是整数
                            result.result = false;
                            result.info = 'value is not an integer';
                        } else {
                            val = [value % 65536];
                        }
                        break;
                    case EnumDataType.Binary:
                        if (/^[0-1]{1,16}$/.test(value + '')) {
                            val = parseInt(value + '', 2);
                        } else {
                            result.result = false;
                            result.info = 'value is illegal';
                        }
                        break;
                    case EnumDataType.Hex:
                        if (/^[0-9A-F]{1,4}$/.test(value + '')) {
                            val = parseInt(value + '', 16);
                        } else {
                            result.result = false;
                            result.info = 'value is illegal';
                        }
                        break;
                    case EnumDataType.Float:
                        try {
                            if (typeof value === 'number') {
                                let buffer = Buffer.alloc(4);
                                buffer.writeFloatBE(value, 0);
                                val = [buffer.readUInt16BE(2), buffer.readUInt16BE(0)];
                            } else {
                                result.result = false;
                                result.info = 'value is not a number';
                            }
                        } catch (e) {
                            result.result = false;
                            result.info = e.message;
                        }
                        break;
                    case EnumDataType.SwappedFloat:
                        try {
                            if (typeof value === 'number') {
                                let buffer = Buffer.alloc(4);
                                buffer.writeFloatBE(value, 0);
                                val = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
                            } else {
                                result.result = false;
                                result.info = 'value is not a number';
                            }
                        } catch (e) {
                            result.result = false;
                            result.info = e.message;
                        }
                        break;
                    case EnumDataType.Long:
                        try {
                            if (typeof value === 'number') {
                                let buffer = Buffer.alloc(4);
                                buffer.writeInt32BE(value, 0);
                                val = [buffer.readUInt16BE(2), buffer.readUInt16BE(0)];
                            } else {
                                result.result = false;
                                result.info = 'value is not a number';
                            }
                        } catch (e) {
                            result.result = false;
                            result.info = e.message;
                        }
                        break;
                    case EnumDataType.SwappedLong:
                        try {
                            if (typeof value === 'number') {
                                let buffer = Buffer.alloc(4);
                                buffer.writeInt32BE(value, 0);
                                val = [buffer.readUInt16BE(0), buffer.readUInt16BE(2)];
                            } else {
                                result.result = false;
                                result.info = 'value is not a number';
                            }
                        } catch (e) {
                            result.result = false;
                            result.info = e.message;
                        }
                        break;
                    case EnumDataType.Double:
                        try {
                            if (typeof value === 'number') {
                                let buffer = Buffer.alloc(8);
                                buffer.writeDoubleBE(value, 0);
                                val = [buffer.readUInt16BE(6), buffer.readUInt16BE(4), buffer.readUInt16BE(2), buffer.readUInt16BE(0)];
                            } else {
                                result.result = false;
                                result.info = 'value is not a number';
                            }
                        } catch (e) {
                            result.result = false;
                            result.info = e.message;
                        }
                        break;
                    case EnumDataType.SwappedDouble:
                        try {
                            if (typeof value === 'number') {
                                let buffer = Buffer.alloc(8);
                                buffer.writeDoubleBE(value, 0);
                                val = [buffer.readUInt16BE(0), buffer.readUInt16BE(2), buffer.readUInt16BE(4), buffer.readUInt16BE(6)];
                            } else {
                                result.result = false;
                                result.info = 'value is not a number';
                            }
                        } catch (e) {
                            result.result = false;
                            result.info = e.message;
                        }
                        break;
                }
            } else {
                result.result = false;
                result.info = 'wrong register type';
            }

            if (result.result) {
                if (this.client.isOpen) {
                    // 已连接
                    if (regType === EnumRegisterType.Coil) {   
                        result.info = await this.client.writeCoil(regAddr, val);
                    } else if (regType === EnumRegisterType.Holding) {
                        result.info = await this.client.writeRegisters(regAddr, val);
                    }
                } else {
                    // 未连接
                    const connRes = await this.connect();
                    if (typeof connRes !== 'string') {
                        // 连接成功
                        if (regType === EnumRegisterType.Coil) {
                            result.info = await this.client.writeCoil(regAddr, val);
                        } else if (regType === EnumRegisterType.Holding) {
                            result.info = await this.client.writeRegisters(regAddr, val);
                        }
                    } else {
                        // 连接失败
                        result.result = false;
                        result.info = 'connect failed';
                    }
                }
            }
            return result;
        }
    }
}